/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package mygame.hudelements;

import com.jme3.asset.AssetManager;
import com.jme3.export.Savable;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import com.jme3.ui.Picture;
import mygame.PlaneBasicControl;

/**
 *
 * @author Aicher
 */
public class HorizonControl extends InstrumentControl implements Savable, Cloneable {
    /**
     * Image id for the instrument frame
     */
    private static final String IMG_HORIZON_INSTRUMENT = "IMG_Horizon_Instrument";
    
    /**
     * Image id for the horizon ground (round brown plate)
     */
    private static final String IMG_HORIZON_GROUND = "IMG_Horizon_Ground";
    
    /**
     * Image id for the horizon sky (blue rectangle which will be resized dependent
     * on the position of the horizon line)
     */
    private static final String IMG_HORIZON_SKY = "IMG_Horzion_Sky";
    
    
    /**
     * Image id for the little arrow indicating the roll angle
     */
    private static final String IMG_HORIZON_ROLLANGLE = "IMG_Horizon_RollAngle";
    
    /**
     * Image id for the little arrow indicating the pitch angle
     */
    private static final String IMG_HORIZON_PITCHANGLE = "IMG_Horizon_PitchAngle";
    
    /**
     * Image id prefix for the horizon tile images, followed by a subsequent number 0 - IMG_LINE_FILE_NAMES.length - 1
     */
    private static final String IMG_HORIZON_TILE = "IMG_Horizon_Tile_";
    
    /**
     * Height of the horizon tile in fraction of the instument height
     */
    private static final float HORIZON_TILE_HEIGHT_FACTOR = 0.08f;
    
    /**
     * Width of the horizon tile in fraction of the instument width
     */
    private static final float HORIZON_TILE_WIDTH_FACTOR = 0.55f;
    
    /**
     * Factor that converts the plane pitch value to movement of the horizon images
     */
    private static final int HORIOZON_PITCH_MOVEMENT_SCALE = 12;
    
    /**
     * Factor that converts the plane pitch value to movement of the pitch arrow
     */
    private static final int PITCH_ARROW_MOVEMENT_SCALE = 70;
    
    /**
     * Constant indicating the horizon up vector points upwards
     */
    private static final int UP = 0;
    
    /**
     * Constant indicating the horizon up vector points to the right
     */
    private static final int RIGHT = 1;
    
    /**
     * Constant indicating the horizon up vector points downwards
     */
    private static final int DOWN = 2;
    
    /**
     * Constant indicating the horizon up vector points to the left
     */
    private static final int LEFT = 3;
    
    /**
     * Number of horizon tile images that should be visible 
     */
    private static final int VISIBLE_NO_OF_TILES = 6;
    
    /**
     * Half number of visible tiles (for avoiding too many unnecessary calculations)
     */
    private static final int VISIBLE_NO_OF_TILES_HALF = 3;
    
    /**
     * Names of the tile image files (starting with the upper file)
     */
    private static final String[] IMG_LINE_FILE_NAMES = {
        "line_35.png",
        "line_30.png",
        "line_25.png",
        "line_20.png",
        "line_15.png",
        "line_10.png",
        "line_5.png",
        "line_0.png",
        "line_5.png",
        "line_10.png",
        "line_15.png",
        "line_20.png",
        "line_25.png",
        "line_30.png",
        "line_35.png"
    };
    
    /**
     * Array holding all tile pictures
     */
    private Picture[] horizonTiles = null;
    
    /**
     * Holds the blue rectangle showing the sky image
     */
    private Picture horizonSky = null;
    
    /**
     * Arrow image showing the horizon pitch. This must be held in an own variable
     * because the picture will be shifted based on the plane pitch
     */
    private Picture horizonPitchAngle = null;
    
    /**
     * Node where the complete horizon elements will be attached to. This node will
     * be moved and rotated, so that the horizon elements will move accordingly.
     */
    private Node horizonNode = null;
    
    /**
     * This node will rotate the horizon rotation arrow for roll indication
     */
    private Node horizonRollRotationPoint = null;
    
    /**
     * Pitch value of the plane (will be y value of the plane z-axis
     */
    private float planePitch = 0f;
    
    /**
     * Variable indicates where the up vector of the horizon elements is pointing. Will
     * be filled with the constants UP, DOWN, LEFT and RIGHT
     */
    private int upPointsTo = 0;
    
    /**
     * Vector which points to the up direction of the artificial horizon. If the plane
     * e.g. is flying upside down, the vector shows downwards.
     */
    private Vector3f horizonUp = new Vector3f(0f, 0f, 0f);
    
    /**
     * Translation vector that transfers the pitch value and the roll value to the
     * propper translation of the artificial horizon line.
     */
    private Vector3f horizonTranslation = new Vector3f(0f, 0f, 0f);
    
    /**
     * For showing/hiding the propper horizon tile images the highest showable
     * tile id is stored here
     */
    private int highestTileID = 0;
    
    /**
     * For showing/hiding the propper horizon tile images the lowest showable
     * tile id is stored here
     */
    private int lowestTileID = 0;
    
    /**
     * This value gives the information, how far the center of the artificial horizon
     * must be moved from the center of the instrument. 
     */
    private float centerShift = 0f;
    
    /**
     * Calculated height of a tile
     */
    private int tileHeight = 0;
    
    /**
     * Calculated width of a tile
     */
    private int tileWidth = 0;
    
    /**
     * Distance, the sky image (blue rectangle) must be shifted from the center
     * of the horizon node. Usually it should be 0, but if the sky is
     * shifted out of the outer instrument border, it must be shifted back - otherwise
     * it would be visible out of the instrument borders.
     */
    private float visibleSkyImageShift = 0;
    
    /**
     * Height of the sky image (blue rectangle) in order to adapt it to the current 
     * position of the center horizon line. If the line is below the instrument center
     * the sky image must be taller - if it is above the line it must be smaller.
     */
    private float visibleSkyImageHeight = 0;
    
    /**
     * Measure of the visible field of the instrument. This is the inner circle,
     * where the artificial horizon is actually visible.
     */
    private float visibleFieldBorder = 0;
    
    /**
     * If the sky image is taller than this value, it must be reduced and shifted.
     */
    private float visibleFieldAlert = 0;
    
    
    /**
     * Constructor
     * @param guiNode Reference to the guiNode
     * @param assetManager For loading the images
     * @param instrumentLeftGridPosX position of the left border (in grid size measures)
     * @param instrumentLowerGridPos position of the lower border (in grid size measures)Y
     * @param hudReferenceSize Sise of one grid measure
     * @param instrumentGridWidth Width of the instrument in grid measures
     * @param instrumentGridHeight  Height of the instrument in grid measures
     * @param screenHeight  Height of the screen in pixel
     * @param measureFromBottom Set to true, if the LowerGridPos should be measured from 
     * the bottom. Set it to fals in order to measure it from the top
     */
    public HorizonControl (Node guiNode,
            AssetManager assetManager,
            float instrumentLeftGridPosX,
            float instrumentLowerGridPosY,
            float hudReferenceSize,
            float instrumentGridWidth,
            float instrumentGridHeight,
            int screenHeight,
            boolean measureFromBottom,
            float minimalCalculationLoopTime
            ) {
        super(guiNode, assetManager,
                instrumentLeftGridPosX,
                instrumentLowerGridPosY,
                hudReferenceSize,
                instrumentGridWidth,
                instrumentGridHeight,
                screenHeight,
                measureFromBottom,
                minimalCalculationLoopTime);
        initHorizon();
    }

    /**
     * Does all initialisation tasks
     */
    private void initHorizon() {
        // prepare the array for holding the tiles
        this.horizonTiles = new Picture[IMG_LINE_FILE_NAMES.length];
        
        // tile height and width are based on the instrument size
        this.tileHeight = (int)(this.instrumentHeight * HORIZON_TILE_HEIGHT_FACTOR);
        this.tileWidth = (int)(this.instrumentHeight * HORIZON_TILE_WIDTH_FACTOR);
        
        // the horizon node, which will be the base node that controls the horizon movement
        // must be located in the middle of the instrument
        this.horizonNode = new Node();
        this.horizonNode.setLocalTranslation(this.instrumentCenterPosX, this.instrumentCenterPosY, 0);
        
        // the first picture must be the horizon ground (brown round image) because it must be at the 
        // background side of the instrument
        this.centralInstrumentNode.attachChild(
                this.createInstrumentElementAbsolute(IMG_HORIZON_GROUND, "Textures/horizon/horizon_ground.png"));
        
        // now prepare the horizon sky image. The with must be like the tiles. The height can
        // be temporarily set to half of the instrument. This value will be recalculated every cycle
        this.horizonSky = new Picture(IMG_HORIZON_SKY);
        this.horizonSky.setImage(assetManager, "Textures/horizon/horizon_blue.png", true);
        this.horizonSky.setWidth(tileWidth);
        this.horizonSky.setHeight(this.instrumentHalfHeight);
        // because the sky wil be located at the upper half of the horizon, the y value must be 0.
        this.horizonSky.setLocalTranslation(-tileWidth / 2, 0, 0);
        this.horizonNode.attachChild(this.horizonSky);
        
        // now attach the horizon node to the central instrument node.
        this.centralInstrumentNode.attachChild(this.horizonNode);
        
        // this value is used more often - so it will be placed into an own variable.
        // It is the half of the height of all tile piled together
        float halfSize = tileHeight * this.horizonTiles.length / 2;
        
        // now load every tile based on the IMG_LINE_FILE_NAMES array and place it propperly
        for (int y = 0; y < this.horizonTiles.length; y++) {
            this.horizonTiles[y] = new Picture(IMG_HORIZON_TILE + y);
            this.horizonTiles[y].setImage(assetManager, "Textures/horizon/" + IMG_LINE_FILE_NAMES[y], true);
            this.horizonTiles[y].setWidth(tileWidth);
            this.horizonTiles[y].setHeight(tileHeight);
            
            // the position must be half tile width (remember - the horizonNode is placed in the 
            // middle of the instrument. The y value is calculated by the current tile multiplied by the
            // tile height minus the calculated half size value (so that the center of the tile pile will
            // be in the center of the horizon node)
            this.horizonTiles[y].setLocalTranslation(-tileWidth/2, 
                    y * tileHeight - halfSize, 0);
            this.horizonNode.attachChild(this.horizonTiles[y]);
        }
        
        // now the instrument image can be placed - so it will hide the tile show/hide mechanic from the user
        this.centralInstrumentNode.attachChild(this.createInstrumentElementAbsolute(
                IMG_HORIZON_INSTRUMENT, "Textures/horizon/horizon_instrument.png"));
        
        // This node will roll according to the plane roll movements - the arrow for the roll
        // indication will be attached to this node
        this.horizonRollRotationPoint = new Node();
        this.horizonRollRotationPoint.setLocalTranslation(this.instrumentCenterPosX, this.instrumentCenterPosY, 0);
        this.centralInstrumentNode.attachChild(this.horizonRollRotationPoint);
        
        // Attach the arrow for the roll indication so that it will rotate around the center point of the instrument
        this.horizonRollRotationPoint.attachChild(this.createInstrumentElementRelativeCenter(
                IMG_HORIZON_ROLLANGLE, "Textures/horizon/horizon_rollangle.png"));
        
        // The pitch angle indicator will be only translated according to the pitch - so we do not need a rotation
        // node for this
        this.horizonPitchAngle = this.createInstrumentElementAbsolute(IMG_HORIZON_PITCHANGLE, 
                "Textures/horizon/horizon_pitchangle.png");
        this.centralInstrumentNode.attachChild(this.horizonPitchAngle);
        
        // now do the calculations for the tile/sky visibility calculations
        this.visibleFieldBorder = this.instrumentWidth / 3;
        this.visibleFieldAlert = this.visibleFieldBorder * 2;
    }
    
    /**
     * In this method all moveable elements of the aritifcial horizon will be calculated. This will be the
     * rotation and translation of the horizon and the movement of the pitch and roll indicators
     */
    private void moveHorizon() {
        // first get the plane pitch from the plane basic control object
        this.planePitch = ((PlaneBasicControl)this.spatial.getControl(PlaneBasicControl.class)).planeAxisZ.y;
        
        // now build the vector that always points to the sky in the artificial horizon. This is done by using
        // the height of the y Axis of the plane pointing up and the negarive height of the x Axis of the plane pointing
        // to the right. If the plane rolls to the left, the height of the y axis decreases and the height of the
        // x axis increases as well - multiplied with -1 it will decrease. So the vector of the artificial 
        // horizon will turn left as well (and vice versa).
        this.horizonUp.x = -((PlaneBasicControl)this.spatial.getControl(PlaneBasicControl.class)).planeAxisX.y;
        this.horizonUp.y = ((PlaneBasicControl)this.spatial.getControl(PlaneBasicControl.class)).planeAxisY.y;
        
        // the vector must be normalized, otherwise the rotateUpTo method would not work properly
        this.horizonUp.normalizeLocal();
        
        // now rotate the node towards the vector and with the node the complete horizon elements. (annotation:
        // i believe that the jitter of the horizon comes from this method - but I'm not shure)
        this.horizonNode.rotateUpTo(this.horizonUp);
        
        // now the translation of the horizon center point can be calculated. It is basically the planePitch. But
        // the value has to be adapted to the current roll value (if the plane is e.g. 90° rolled, the shift must
        // be conducted in the x direction, not y). So the pitch is multiplied by the horizonUp.x value and a scaling
        // factor based on the tileHeight and the HORIOZON_PITCH_MOVEMENT_SCALE value. The tile height value was introduced
        // in order to move relative to the tile size
        this.horizonTranslation.x = this.instrumentCenterPosX - 
                this.planePitch * this.tileHeight * HORIOZON_PITCH_MOVEMENT_SCALE * this.horizonUp.x;
        this.horizonTranslation.y = this.instrumentCenterPosY - 
                this.planePitch * this.tileHeight * HORIOZON_PITCH_MOVEMENT_SCALE * this.horizonUp.y;

        // now translate the complete node according to the calculated value
        this.horizonNode.setLocalTranslation(this.horizonTranslation);
        
        // For later calculation we need to know, which direction the horizon up vector is pointing to. 
        // therefore we need to check the y and x values (0.7 is sin 45°)
        if ((this.horizonUp.y > -0.7) && (this.horizonUp.y < 0.7)) {
            if (this.horizonUp.x < 0) {
                this.upPointsTo = LEFT;
            } else {
                this.upPointsTo = RIGHT;
            }
        } else {
            if (this.horizonUp.y < 0) {
                this.upPointsTo = DOWN;
            } else {
                this.upPointsTo = UP;
            }
        }

        // this value indicates, how far the center of the horizon must be shifted from the center of the instrument
        // Therefore we need to calculate this size via the center minus the translation vector (with pythagoras)
        this.centerShift = FastMath.sqrt(FastMath.sqr(this.instrumentCenterPosX - this.horizonTranslation.x)
                + FastMath.sqr(this.instrumentCenterPosY - this.horizonTranslation.y));
        
        // the last initialization is the highest tileId which will be in the extreme case the highest array index
        this.highestTileID = this.horizonTiles.length - 1;
        
        // now calculate the values based on the quadrant of the up pointing horizon vector
        switch(this.upPointsTo){
            case LEFT:
                
                // first calculate the image height of the sky image. This will be the visible field border (assuming that
                // the horizon is placed in the middle) plus the center shift. However, because the center shift is
                // due to the pythagoras always positive, we need to derive the direction from the x position of the 
                // horizon relative to the instrument center by using the signum function
                this.visibleSkyImageHeight = this.visibleFieldBorder + 
                        FastMath.sign(this.horizonTranslation.x - this.instrumentCenterPosX) * centerShift;
                
                // now calculate the lowest tile id. This is the half position index (highestTileID / 2) minus the
                // half visible number of tiles. This value must be corrected by the centerShift value with the calculated sign
                // like in the image height calculation divided by the tileHeight (wich results in the number of tiles fitting
                // into the centerShift size). 
                this.lowestTileID = this.highestTileID / 2 - VISIBLE_NO_OF_TILES_HALF + (int)(
                        FastMath.sign(this.horizonTranslation.x - this.instrumentCenterPosX) * this.centerShift / this.tileHeight);
                
                // the highestTileID will be now overwritten by the lowestTileID plus the number of visible tiles.
                this.highestTileID =  this.lowestTileID + VISIBLE_NO_OF_TILES;
                break;
            case RIGHT:
                // the right calculation is like the one for left, with inverted direction
                this.visibleSkyImageHeight = this.visibleFieldBorder - 
                        FastMath.sign(this.horizonTranslation.x - this.instrumentCenterPosX) * centerShift;
                this.lowestTileID = this.highestTileID / 2 - VISIBLE_NO_OF_TILES_HALF - (int)(
                        FastMath.sign(this.horizonTranslation.x - this.instrumentCenterPosX) * this.centerShift / this.tileHeight);
                this.highestTileID =  this.lowestTileID + VISIBLE_NO_OF_TILES;
                break;
            case DOWN:
                // the down calculation is like the one for left, with the difference that we are using the y coordinates, not x
                this.visibleSkyImageHeight = this.visibleFieldBorder + 
                        FastMath.sign(this.horizonTranslation.y - this.instrumentCenterPosY) * centerShift;
                this.lowestTileID = this.highestTileID / 2 - VISIBLE_NO_OF_TILES_HALF + (int)(
                        FastMath.sign(this.horizonTranslation.y - this.instrumentCenterPosY) * this.centerShift / this.tileHeight);
                this.highestTileID =  this.lowestTileID + VISIBLE_NO_OF_TILES;
                break;
            case UP:
                // the up calculation is like the one for down, with inverted direction
                this.visibleSkyImageHeight = this.visibleFieldBorder - 
                        FastMath.sign(this.horizonTranslation.y - this.instrumentCenterPosY) * centerShift;
                this.lowestTileID = this.highestTileID / 2 - VISIBLE_NO_OF_TILES_HALF - (int)(
                        FastMath.sign(this.horizonTranslation.y - this.instrumentCenterPosY) * this.centerShift / this.tileHeight);
                this.highestTileID =  this.lowestTileID + VISIBLE_NO_OF_TILES;
                break;
        }
        
        // now we have to check, if the visible sky image height exceeds the visible border of our instrument.
        // This situation will occur, if the lower border of the sky image will be shifted out of the instrument - like
        // in a flight upward with a very high angle. The horizon will show only sky. The lower part of the sky must be
        // cut off. So the sky image will be shifted back behind the visible Field border and resized to the visible
        // size of the instrument.
        if (this.visibleSkyImageHeight > this.visibleFieldAlert) {
            this.visibleSkyImageShift = this.visibleSkyImageHeight - this.visibleFieldAlert;
            this.visibleSkyImageHeight = this.visibleFieldAlert;
        } else {
            this.visibleSkyImageShift = 0;
        }
        // Now do the correction of the sky image
        this.horizonSky.setLocalTranslation(-this.tileWidth / 2, this.visibleSkyImageShift, 0);
        this.horizonSky.setHeight(this.visibleSkyImageHeight);
        
        // in this loop the visible horizonTiles will be showed/hidden based on the highest/lowest Tile IDs
        for (int i = 0; i < this.horizonTiles.length; i++) {
            if ((i <= this.highestTileID) && (i >= this.lowestTileID)) {
                this.horizonTiles[i].setCullHint(Spatial.CullHint.Inherit);
            } else {
                this.horizonTiles[i].setCullHint(Spatial.CullHint.Always);
            }
        }
        
        // Rotate the the roll indicator according to the up vector
        this.horizonRollRotationPoint.rotateUpTo(this.horizonUp);
        
        // Shift the pitch indicator according to the plane pitch. This must be scaled by a separate
        // factor in order to have a propper arrow movement.
        // Attention - in a next video we must attach this value to the instrument size, because
        // the value will differ depending on the selected instrument size!
        this.horizonPitchAngle.setLocalTranslation(this.instrumentLeftPosX, 
                this.instrumentLowerPosY - PITCH_ARROW_MOVEMENT_SCALE * this.planePitch, 0);
         
    }
    
    @Override
    protected void controlUpdate(float tpf) {
        // same procedure as in all other controlUpdate methods...
        this.timeSinceLastUpdate += tpf;
        if (this.timeSinceLastUpdate > this.minimalCalculationLoopTime) {
        moveHorizon();
            this.timeSinceLastUpdate = 0f;
        }
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        // not needec yet
    }
    
    @Override
    public Control cloneForSpatial(Spatial theSpatial) {
        final HorizonControl control = new HorizonControl(
                guiNode, 
                assetManager,
                instrumentLeftGridPosX,
                instrumentLowerGridPosY,
                hudReferenceSize,
                instrumentGridWidth,
                instrumentGridHeight,
                screenHeight,
                measureFromBottom,
                minimalCalculationLoopTime);
        
        control.setSpatial(spatial);
        return control;
    }      
    
}
